Ana içeriğe geç

Rust İşlevsel Makroları

Özet

  • İşlevsel makrolar, programcının özel girdi bilgisine dayanarak derleme zamanında kod oluşturmasına olanak tanıyan özel bir Rust makrosudur.
  • Anchor çerçevesinde, işlevsel makrolar, Solana programları yazarken gereken gereksiz kodu azaltan kodlar üretir.
  • Soyut Sözdizim Ağacı (AST), bir işlevsel makroya geçirilen giriş kodunun sözdizimini ve yapısını temsil eder. Bir makro oluştururken, uygun kodu oluşturmak için token ve öğe gibi AST öğelerini kullanırsınız.
  • Token, Rust derleyicisinin ayrıştırabileceği en küçük kaynak kodu birimidir.
  • Öğe, bir Rust programında bir yapı, enum, trait, fonksiyon veya yöntem gibi kullanılabilecek bir şeyi tanımlayan bir bildirimdir.
  • TokenStream, bir kaynak kodu parçasını temsil eden token dizisidir. Bunu bir işlevsel makroya geçirebilir, böylece kodda bireysel token'lara erişip bunları manipüle edebilirsiniz.

Ders

Rust'ta bir makro, bir kez yazıp ardından "genişletip" derleme zamanında kod oluşturabileceğiniz bir kod parçasıdır. Bu kod oluşturma işlemi, tekrarlayan veya karmaşık kodlar oluşturmanız gerektiğinde veya programınızda birçok yerde aynı kodu kullanmak istediğinizde faydalı olabilir.

İki farklı makro türü vardır: deklaretif makrolar ve işlevsel makrolar.

  • Deklaretif makrolar, kod kalıplarıyla eşleşip, eşleşen kalıba dayanarak kod oluşturmanıza olanak tanıyan macro_rules! makrosu kullanılarak tanımlanır.
  • Rust'taki işlevsel makrolar, Rust kodu kullanılarak tanımlanır ve giriş TokenStream'inin soyut sözdizim ağacı (AST) üzerinde çalışır; bu da onlara daha ince bir ayrıntı seviyesinde kodu manipüle etme ve oluşturma yeteneği kazandırır.
bilgi

Bu ders, Anchor çerçevesinde standart olan işlevsel makrolara odaklanacaktır.

Rust Kavramları

Özellikle makroları tartışmadan önce, ders boyunca kullanacağımız bazı önemli terminoloji, kavramlar ve araçları gözden geçirelim.

Token

Rust programlamasında, bir token, dilin sözdiziminin temel bir unsurudur; bir tanımlayıcı veya literal değer gibi. Token'lar, Rust derleyicisi tarafından tanınan en küçük kaynak kodu birimini temsil eder ve bir programdaki daha karmaşık ifadeleri ve beyanları oluşturmak için kullanılır.

Rust token'larının örnekleri şunlardır:

  • fn, let ve match gibi Anahtar Kelimeler, Rust dilinde özel anlamlara sahip ayrılmış kelimelerdir.
  • Değişken ve fonksiyon adları gibi, Tanımlayıcılar, değerlere ve fonksiyonlara işaret eder.
  • {, }, ve ; gibi Noktalama İşaretleri, kod bloklarını yapılandırmak ve sınırlamak için kullanılır.
  • Sayılar ve dizeler gibi Literal'lar, bir Rust programındaki sabit değerleri temsil eder.

Daha fazla bilgi edinebilirsiniz Rust token'ları hakkında daha fazla bilgi okuyabilirsiniz.

Öğe

Öğeler, Rust'taki adlandırılmış ve kendi kendine yeterli kod parçalarıdır. İlgili kodları grup haline getirmenin ve bu gruba referans vermek için bir isim vermenin bir yolunu sağlar; bu da kodunuzu modüler olarak yeniden kullanmanızı ve düzenlemenizi kolaylaştırır.

Farklı türde öğeler vardır, örneğin:

  • Fonksiyonlar
  • Yapılar
  • Enumlar
  • Trait'ler
  • Modüller
  • Makrolar

Daha fazla bilgi edinebilirsiniz Rust öğeleri hakkında.

Token Stream'ler

TokenStream veri türü, bir token dizisini temsil eder. proc_macro kütüphanesinde tanımlanmıştır ve makroların, kod tabanındaki diğer kodlardan yola çıkarak yazılmasını sağlamak için sunulur.

not

Bir işlevsel makro tanımlarken, makro girişi TokenStream olarak makroya geçirilir; bu da ardından ayrıştırılıp dönüştürülür.

Elde edilen TokenStream, ardından makro tarafından nihai kod çıktısına genişletilebilir.

use proc_macro::TokenStream;

#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream {
...
}

Soyut Sözdizim Ağacı

Rust işlevsel makro bağlamında, soyut sözdizim ağacı (AST), giriş token'larının hiyerarşik yapısını ve Rust dilindeki anlamını temsil eden bir veri yapısıdır. Genellikle, işlevsel makro tarafından hızlı bir şekilde işlenip dönüştürülebilen girişin ara temsili olarak kullanılır.

tehlike

Makro, AST'yi kullanarak giriş kodunu analiz edebilir ve değişiklikler yapabilir; örneğin token ekleyip çıkarabilir veya kodun anlamını dönüştürebilir.

Daha sonra bu dönüştürülmüş AST'yi kullanarak yeni kod oluşturabilir; bu da proc makrosunun çıktısı olarak dönebilir.

syn Kütüphanesi

syn kütüphanesi, bir token akışını makro kodunun gezip manipüle edebileceği bir AST'ye ayrıştırmaya yardımcı olmak için kullanılabilir. Rust programında bir işlevsel makro çağrıldığında, makro işlevi bir token akışı ile çağrılır. Bu girişin ayrıştırılması, neredeyse her makro için ilk adımdır.

Bir proc makrosunu, aşağıdaki şekilde my_macro! kullanarak çağırdığınız bir örnek olarak düşünün:

my_macro!("hello, world");

Yukarıdaki kod çalıştırıldığında, Rust derleyicisi giriş token'larını ("hello, world") my_macro proc makrosuna TokenStream olarak geçirir.

use proc_macro::TokenStream;
use syn::parse_macro_input;

#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as syn::LitStr);
eprintln!("{:#?}", ast.token());
...
}

Proc makrosu içinde, kod parse_macro_input! makrosunu kullanarak giriş TokenStream'ini bir soyut sözdizim ağacına (AST) ayrıştırır. Özellikle, bu örnek, Rust'taki bir UTF-8 dize literalini temsil eden LitStr örneği olarak ayrıştırmaktadır. AST'nin hata ayıklama amaçlarıyla çıktısını görmek için .token() yöntemini kullanarak bir Literal döndürürüz.

Literal {
kind: Str,
symbol: "hello, world",
suffix: None,
// `TokenStream`'in ayrıştırıldığı kaynak kodunun kısmında
// "hello, world" literalinin byte offsetlerini 31 ile 45 arasında gösterir.
span: #0 bytes(31..45),
}

eprintln! makrosunun çıktısı, özgün token'lardan oluşturulan Literal AST'sinin yapısını gösterir. Dize literal değerini ("hello, world") ve token hakkında, türü (Str), ek (None) ve genişleme gibi diğer bilgileri gösterir.

quote Kütüphanesi

Bir diğer önemli kütüphane de quote kütüphanesidir; kod oluşturma kısmında kritik bir rol oynar.

Bir proc makrosu, AST'yi analiz etme ve dönüştürme işlemini bitirdikten sonra, quote kütüphanesini veya benzer bir kod oluşturma kütüphanesini kullanarak token akışına geri dönüştürebilir. Sonrasında, orijinal akışın kaynak kodundaki yerine geçmesi için Rust derleyicisine döndürülür.

Aşağıdaki my_macro örneğini inceleyelim:

use proc_macro::TokenStream;
use syn::parse_macro_input;
use quote::quote;

#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as syn::LitStr);
eprintln!("{:#?}", ast.token());
let expanded = quote! {println!("The input is: {}", #ast)};
expanded.into()
}
ipucu

Bu örnek, quote! makrosunu kullanarak LitStr AST'sini argüman olarak alan bir println! makro çağrısından oluşan yeni bir TokenStream oluşturur.

Dikkat edin, quote! makrosu, proc_macro2::TokenStream türünde bir TokenStream üretir. Bu TokenStream'i Rust derleyicisine döndürmek için, proc_macro::TokenStream'e dönüştürmek için .into() metodunu kullanın. Rust derleyicisi daha sonra bu TokenStream'i orijinal proc makro çağrısının yerine koyacaktır.

The input is: hello, world

İşlevsel makrolar kullanarak güçlü kod oluşturma ve metaprogramlama görevleri gerçekleştiren işlevsel makrolar yaratabilirsiniz.

İşlevsel Makro

Rust'taki işlevsel makrolar, dili genişletmenin ve özel sözdizimi oluşturmanın güçlü bir yoludur. Bu makrolar Rust'ta yazılır ve kodun geri kalanıyla birlikte derlenir. Üç tür işlevsel makro vardır:

  • Fonksiyon benzeri makrolar - custom!(...)
  • Derivasyon makroları - #[derive(CustomDerive)]
  • Attribute makroları - #[CustomAttribute]
bilgi

Bu bölüm, üç tür işlevsel makroyu tartışacak ve birine örnek uygulama sağlayacaktır.

Bir işlevsel makro yazmak, tüm üç türde de tutarlı olduğundan, bu örnek diğer türlere uyarlanabilir.

Fonksiyon Benzeri Makrolar

Fonksiyon benzeri işlevsel makrolar, üç tür işlevsel makro arasında en basitidir. Bu makrolar, #[proc_macro] niteliği ile önceden tanımlanmış bir fonksiyon kullanılarak tanımlanır. Fonksiyon, bir TokenStream almalı ve orijinal kodun yerine geçecek yeni bir TokenStream döndürmelidir.

#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream {
...
}
not

Bu makrolar, fonksiyon adının ardından ! operatörü ile çağrılır. Rust programında ifadeler, beyanlar ve fonksiyon tanımları gibi çeşitli yerlerde kullanılabilirler.

my_macro!(input);

Fonksiyon benzeri işlevsel makrolar, yalnızca tek bir girdi ve çıktı akışı gerektiren basit kod oluşturma görevleri için en iyi şekilde uygundur. Anlaşılması ve kullanılması kolaydır ve derleme zamanında kod oluşturmak için doğrudan bir yol sağlar.

Attribute Makroları

Attribute makroları, Rust programındaki öğelere, örneğin fonksiyonlar ve yapılar gibi, eklenen yeni nitelikler tanımlar.

#[my_macro]
fn my_function() {
...
}

Attribute makroları, #[proc_macro_attribute] niteliği ile önceden tanımlanmış bir fonksiyon ile tanımlanır. Fonksiyon, iki token akışını girdi olarak alır ve orijinal öğeyi, arzu edilen sayıda yeni öğe ile değiştiren tek bir TokenStream çıktısı döndürür.

#[proc_macro_attribute]
pub fn my_macro(attr: TokenStream, input: TokenStream) -> TokenStream {
...
}

İlk token akışı girişi, niteliğin argümanlarını temsil eder. İkinci token akışı, niteliğin eklendiği öğenin geri kalanını, mevcut olabilecek diğer nitelikler dahil.

#[my_macro(arg1, arg2)]
fn my_function() {
...
}

Örneğin, bir attribute makrosu, kendisine geçirilen argümanları işleyerek belirli özellikleri açıp kapatabilir ve ikinci token akışını kullanarak orijinal öğeyi değiştirebilir. Her iki token akışına erişim sağlamak, attribute makrolarının tek bir token akışı kullanmaktan daha fazla esneklik ve işlevsellik sağlamasına olanak tanır.

Derivasyon Makroları

Derivasyon makroları, bir yapı, enum veya birliğe #[derive] niteliği kullanılarak çağrılır. Genellikle, otomatik olarak giriş türleri için trait'leri uygulamak için kullanılırlar.

#[derive(MyMacro)]
struct Input {
field: String
}

Derivasyon makroları, bir işlem öncesinde #[proc_macro_derive] niteliği ile tanımlanmış bir fonksiyon ile belirlenir. Bu, yalnızca yapıların, enumların ve birliklerin kodunu üretmek için sınırlıdır. Tek bir token akışı alır ve bir tek token akışı döndürür.

Diğer işlevsel makrolardan farklı olarak, döndürülen token akışı orijinal kodu değiştirmez. Bunun yerine, orijinal öğenin ait olduğu modül veya blokta eklenir; bu da geliştiricilerin orijinal kodu değiştirmeden orijinal öğenin işlevselliğini genişletmelerine olanak tanır.

#[proc_macro_derive(MyMacro)]
pub fn my_macro(input: TokenStream) -> TokenStream {
...
}

Trait'leri uygulamanın yanı sıra, derivasyon makroları yardımcı nitelikler de tanımlayabilir. Yardımcı nitelikler, derivasyon makrosunun eklendiği öğenin kapsamı içinde kullanılabilir ve kod oluşturma sürecini özelleştirebilir.

#[proc_macro_derive(MyMacro, attributes(helper))]
pub fn my_macro(body: TokenStream) -> TokenStream {
...
}

Yardımcı nitelikler, kendi başlarına etkisi olmayan inaktif niteliklerdir. Tek amacı, tanımlandıkları derivasyon makrosuna girdi olarak kullanılmaktır.

#[derive(MyMacro)]
struct Input {
#[helper]
field: String
}

Örneğin, bir derivasyon makrosu, varlığını belirli operasyonlar gerçekleştirmek için tanımlamak üzere bir yardımcı niteliği belirleyebilir ve geliştiricilerin derivasyon makrosunun işlevselliğini daha esnek bir şekilde genişletmesine ve oluşturdukları kodu özelleştirmesine olanak tanır.

Bir işlevsel makro örneği

Bu örnek, bir yapı için otomatik olarak describe() yönteminin uygulanımını nasıl oluşturacağınızı göstermektedir.

use example_macro::Describe;

#[derive(Describe)]
struct MyStruct {
my_string: String,
my_number: u64,
}

fn main() {
MyStruct::describe();
}

describe() yöntemi, yapının alanlarının tanımını konsola yazdıracaktır.

MyStruct, şu adlandırılmış alanlara sahip bir yapıdır: my_string, my_number.

İlk adım, işlevsel makroyu #[proc_macro_derive] niteliği ile tanımlamaktır. Yapının tanımlayıcısını ve verilerini çıkarmak için giriş TokenStream'i parse_macro_input!() makrosu ile ayrıştırılır.

use proc_macro::{self, TokenStream};
use quote::quote;
use syn::{parse_macro_input, DeriveInput, FieldsNamed};

#[proc_macro_derive(Describe)]
pub fn describe_struct(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input);
...
}

Bir sonraki adım, data değerini match anahtar kelimesi ile pattern matching uygulayarak yapının alanlarının adlarını çıkarmaktır.

İlk match iki kola sahiptir: biri syn::Data::Struct varyantı için ve diğeri syn::Data'nın diğer tüm varyantlarını yöneten "yakala" _ koludur.

İkinci match için de iki kola sahiptir: biri syn::Fields::Named varyantı için ve diğeri syn::Fields'ın diğer tüm varyantlarını yöneten "yakala" _ koludur.

#(#idents), * sözdizimi, idents'in "genişletileceği" ve iterator'daki elemanların virgülle ayrılmış bir listesinin oluşturulacağı anlamına gelir.

use proc_macro::{self, TokenStream};
use quote::quote;
use syn::{parse_macro_input, DeriveInput, FieldsNamed};

#[proc_macro_derive(Describe)]
pub fn describe_struct(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input);

let field_names = match data {
syn::Data::Struct(s) => match s.fields {
syn::Fields::Named(FieldsNamed { named, .. }) => {
let idents = named.iter().map(|f| &f.ident);
format!(
"adlandırılmış alanlara sahip bir yapı: {}",
quote! {#(#idents), *},
)
}
_ => panic!("syn::Fields varyantı desteklenmiyor"),
},
_ => panic!("syn::Data varyantı desteklenmiyor"),
};
...
}

Son adım, bir yapı için describe() yöntemini uygulamak. expanded değişkeni quote! makrosunu ve yapının adını saklayan #ident değişkenini kullanarak oluşturulur.

Bu uygulama, yapının adını ve alan adlarını yazdırmak için println! makrosunu kullanan describe() yöntemini tanımlar.

Sonunda, expanded değişkeni TokenStream'e dönüştürülür.

use proc_macro::{self, TokenStream};
use quote::quote;
use syn::{parse_macro_input, DeriveInput, FieldsNamed};

#[proc_macro_derive(Describe)]
pub fn describe(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input);

let field_names = match data {
syn::Data::Struct(s) => match s.fields {
syn::Fields::Named(FieldsNamed { named, .. }) => {
let idents = named.iter().map(|f| &f.ident);
format!(
"adlandırılmış alanlara sahip bir yapı: {}",
quote! {#(#idents), *},
)
}
_ => panic!("syn::Fields varyantı desteklenmiyor"),
},
_ => panic!("syn::Data varyantı desteklenmiyor"),
};

let expanded = quote! {
impl #ident {
fn describe() {
println!("{} şu şekildedir: {}.", stringify!(#ident), #field_names);
}
}
};

expanded.into()
}

Artık, bir yapıya #[derive(Describe)] niteliği eklendiğinde, Rust derleyicisi otomatik olarak describe() yönteminin bir uygulamasını oluşturur; bu yöntem, yapının adını ve alanlarını yazdırmak için kullanılabilir.

#[derive(Describe)]
struct MyStruct {
my_string: String,
my_number: u64,
}

cargo expand komutu, işlevsel makroları kullanan Rust kodunu genişletebilir. Örneğin, #[derive(Describe)] niteliğini kullanan MyStruct yapısının oluşturduğu kod aşağıdaki gibidir:

struct MyStruct {
my_string: String,
my_number: f64,
}
impl MyStruct {
fn describe() {
{
::std::io::_print(
::core::fmt::Arguments::new_v1(
&["", " şu şikilde: ", ".\n"],
&[
::core::fmt::ArgumentV1::new_display(&"MyStruct"),
::core::fmt::ArgumentV1::new_display(
&"adlandırılmış alanlara sahip bir yapı: my_string, my_number",
),
],
),
);
};
}
}

Anchor İşlevsel Makroları

İşlevsel makrolar, genellikle Solana geliştirmelerinde kullanılan Anchor kütüphanesinin arkasındaki sihirdir. Anchor makroları daha kısa kod, standart güvenlik kontrolleri ve daha fazlası sağlar. Anchor'ın işlevsel makroları nasıl kullandığını birkaç örnekle inceleyelim.

Fonksiyon Benzeri Makro

declare_id makrosu, Anchor'da fonksiyon benzeri makroların nasıl kullanıldığını göstermektedir. Bu makro, bir programın kimliğini temsil eden karakterler dizisini girdi olarak alır ve bunu Anchor programında kullanılabilecek bir Pubkey türüne dönüştürür.

declare_id!("G839pmstFmKKGEVXRGnauXxFgzucvELrzuyk6gHTiK7a");

declare_id makrosu, #[proc_macro] niteliği ile tanımlanmıştır; bu da onun bir fonksiyon benzeri proc makrosu olduğunu belirtir.

#[proc_macro]
pub fn declare_id(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let address = input.clone().to_string();

let id = parse_macro_input!(input as id::Id);
let ret = quote! { #id };
...
let idl_print = anchor_syn::idl::gen_idl_print_fn_address(address);
return proc_macro::TokenStream::from(quote! {
#ret
#idl_print
});
...
}

Derivasyon Makrosu

#[derive(Accounts)], Anchor'da kullanılan birden çok derivasyon makrosundan sadece bir örnektir.

#[derive(Accounts)] makrosu, verilen yapı için Accounts trait'ini uygulayan kodlar üretir. Bu trait, birçok şeyi gerçekleştirir, bunlar arasında bir talebe aktarılacak hesapların doğrulanması ve serileştirilmesi yer alır; bu da yapının bir Anchor programında bir talebe ihtiyaç duyan hesapların listesi olarak kullanılmasına olanak tanır.

#[account(..)] niteliği ile belirtilen alanlardaki herhangi bir kısıtlama, serileştirme sırasında uygulanır. #[instruction(..)] niteliği de eklenerek talebin argümanlarını belirtir ve bunların makroya erişilebilir olmasını sağlar.

#[derive(Accounts)]

[instruction(input: String)]

pub struct Initialize { #[account(init, payer = payer, space = MyData::DISCRIMINATOR.len() + MyData::INIT_SPACE + input.len())] pub data_account: Account, #[account(mut)] pub payer: Signer, pub system_program: Program, }

ipucu

Bu makro, bir struct'a uygulanabilecek bir türetme makrosu olarak kullanılmasını sağlayan proc_macro_derive niteliği kullanılarak tanımlanmıştır.

#[proc_macro_derive(Accounts, attributes(account, instruction))] satırı, bunun account ve instruction yardımcı niteliklerini işleyen bir türetme makrosu olduğunu belirtir.

INIT_SPACE, bir hesabın başlangıç boyutunu hesaplamak için kullanılır. Bu, otomatik olarak MyData üzerinde türetme makrosu aracılığıyla uygulanmış ve anchor_lang::Space ile tanımlanmıştır.

#[account]
#[derive(InitSpace)]
pub struct NewAccount {
data: u64,
}

#[account] makrosu ayrıca, anchor_lang::Discriminator niteliğini uygulayan bir anchor hesabının DISCRIMINANTI’ni otomatik olarak türetir. Bu nitelik, ayırt edici olanı içeren 8 baytlık bir dizi sunar; bu, NewAccount::DISCRIMINATOR kullanılarak açığa çıkarılabilir. Bu 8 baytlık dizideki .len() çağrısı, ayırt edicinin uzunluğunu verir;

#[proc_macro_derive(Accounts, attributes(account, instruction))]
pub fn derive_anchor_deserialize(item: TokenStream) -> TokenStream {
parse_macro_input!(item as anchor_syn::AccountsStruct)
.to_token_stream()
.into()
}

Niteliği makrosu #[program]

#[program] niteliği makrosu, Anchor'da bir Solana programı için talimat yöneticilerini içeren modülü tanımlamak için kullanılan bir niteliği makrosunun örneğidir.

#[program]
pub mod my_program {
use super::*;

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
...
}
}
bilgi

Bu durumda, #[program] niteliği, bir modüle uygulanmıştır; bu modülün bir Solana programı için talimat yöneticilerini içerdiğini belirtir.

#[proc_macro_attribute]
pub fn program(
_args: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
parse_macro_input!(input as anchor_syn::Program)
.to_token_stream()
.into()
}

Genel olarak, Anchor'da proc makrolarını kullanmak, Solana geliştiricilerinin yazması gereken tekrarlayan kod miktarını önemli ölçüde azaltır. Kodu temel işlevselliğe odaklanarak tekrarı azaltarak, hataları minimuma indirmek ve daha hızlı bir geliştirme süreci sağlamak mümkündür.


Laboratuvar

Bunu yeni bir türetme makrosu oluşturarak pratiğe dökelim! Yeni makromuz, bir Anchor programındaki bir hesabın her alanını güncelleme işlemi için otomatik olarak talimat mantığı oluşturmamıza izin verecek.

1. Başlangıç

Başlamak için, anchor-custom-macro deposunun starter dalından başlangıç kodunu indirin.

Başlangıç kodu, bir Config hesabını başlatmanıza ve güncellemenize izin veren basit bir Anchor programı içerir; bu, Program Yapılandırması dersi ile yaptığımızın benzeridir.

not

Söz konusu hesap aşağıdaki gibi yapılandırılmıştır:

use anchor_lang::{Discriminator, prelude::*};

#[account]
#[derive(InitSpace)]
pub struct Config {
pub auth: Pubkey,
pub bool: bool,
pub first_number: u8,
pub second_number: u64,
}

impl Config {
pub const LEN: usize = Config::DISCRIMINATOR.len() + Config::INIT_SPACE;
}

programs/admin/src/lib.rs dosyası, programın talimatlarının tanımları ile birlikte program giriş noktası içerir. Şu anda program, bu hesabı başlatmak için talimatlara ve ardından alanı güncellemek için her hesaba bir talimat içerir.

programs/admin/src/admin_config dizini, programın talimat mantığı ve durumunu içerir. Bu dosyaların her birine göz atın. Her alan için talimat mantığının her talimat için tekrarlandığını göreceksiniz.

tehlike

Bu laboratuvarın amacı, talimat mantığı işlevlerini değiştirecek ve her talimat için otomatik olarak işlevler oluşturacak bir işlemsel makro uygulamaktır.

2. Özel makro bildirimini ayarlama

Özel makromuz için ayrı bir crate oluşturarak başlayalım. Proje kök dizininde cargo new --lib custom-macro komutunu çalıştırın. Bu komut, kendine ait Cargo.toml dosyasıyla yeni bir custom-macro dizini oluşturur. Yeni Cargo.toml dosyasını aşağıdaki gibi güncelleyin:

[package]
name = "custom-macro"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
syn = "2.0.77"
quote = "1.0.73"
proc-macro2 = "1.0.86"
anchor-lang.workspace = true

proc-macro = true satırı, bu crate'in bir prosedürel makro içerdiğini tanımlar. Bağımlılıklar, türetme makromuzu oluşturmak için kullanacağımız tüm crate'lerdir.

Sonrasında, proje kökündeki Cargo.toml dosyasının members alanını "custom-macro" içercek şekilde güncelleyin:

[workspace]
members = [
"programs/*",
"custom-macro"
]

[workspace.dependencies]
anchor-lang = "0.30.1"
not

[workspace.dependencies] alanında anchor-lang bağımlılık olarak tanımlandığında, kök proje yapılandırmasında anchor-lang versiyonunu tanımlayıp, onun üzerine bağımlı olan aynı workspace'in diğer tüm üyelerinde o versiyonu miras alabiliriz. Tıpkı bir sonraki tanımlanacak custom-macro-test crate'i gibi.

Şimdi, crate'imiz ayarlandı ve kullanılmaya hazır. Ama ilerlemeden önce, yaratmakta olduğumuz makroyu test etmek için kök seviyede bir crate daha oluşturalım. Proje kökünde cargo new custom-macro-test komutunu kullanın. Ardından yeni oluşturulan Cargo.toml dosyasını anchor-lang ve custom-macro crate'lerini bağımlılıklar olarak ekleyecek şekilde güncelleyin:

[package]
name = "custom-macro-test"
version = "0.1.0"
edition = "2021"

[dependencies]
anchor-lang.workspace = true
custom-macro = { path = "../custom-macro" }

Son olarak, kök proje Cargo.toml dosyasını yeni custom-macro-test crate'ini daha önceki gibi ekleyecek şekilde güncelleyin:

[workspace]
members = [
"programs/*",
"custom-macro",
"custom-macro-test"
]

Son olarak, custom-macro-test/src/main.rs dosyasındaki kodu aşağıdaki gibi değiştirin. Bunu test etmek için daha sonra kullanacağız:

use anchor_lang::prelude::*;
use custom_macro::InstructionBuilder;

#[derive(InstructionBuilder)]
pub struct Config {
pub auth: Pubkey,
pub bool: bool,
pub first_number: u8,
pub second_number: u64,
}

3. Özel makroyu tanımlama

Şimdi, custom-macro/src/lib.rs dosyasında yeni makromuzun bildirimini ekleyelim. Bu dosyada, TokenStream girdisini analiz etmek ve bir DeriveInput yapısından ident ve data alanlarını çıkartmak için parse_macro_input! makrosunu kullanacağız. Ardından, ident ve data değerlerini yazdırmak için eprintln! makrosunu kullanacağız. Şimdi TokenStream::new() kullanarak boş bir TokenStream döneceğiz.

use proc_macro::TokenStream;
use quote::*;
use syn::*;

#[proc_macro_derive(InstructionBuilder)]
pub fn instruction_builder(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input);

eprintln! ("{:#?}", ident);
eprintln! ("{:#?}", data);

TokenStream::new()
}

Bunun neyi yazdırdığına test yapalım. Bunun için önce cargo-expand komutunu kurmalısınız; cargo install cargo-expand komutunu çalıştırarak bunu yapabilirsiniz. Ayrıca, Rust'un gece yüklemesini de çalıştırmanız gerekiyor; rustup install nightly komutunu kullanın.

Bunları yaptıktan sonra, custom-macro-test dizinine gidin ve cargo expand çalıştırın.

not

Bu komut, crate'teki makroları genişletir. main.rs dosyası yeni oluşturulan InstructionBuilder makrosunu kullandığı için, bu, yapı tanımının ident ve data için sözdizim ağacını konsola yazdıracaktır. Girdi TokenStream'inin doğru bir şekilde analiz edilip edilmediğini onayladıktan sonra, eprintln! ifadelerini kaldırabilirsiniz.

4. Yapının alanlarını alma

Sonraki adımda, yapının data alanından adlandırılmış alanları almak için match ifadelerini kullanalım. Ardından, alanların değerlerini yazdırmak için eprintln! makrosunu kullanacağız.

use proc_macro::TokenStream;
use quote::*;
use syn::*;

#[proc_macro_derive(InstructionBuilder)]
pub fn instruction_builder(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input);

let fields = match data {
syn::Data::Struct(s) => match s.fields {
syn::Fields::Named(n) => n.named,
_ => panic!("The syn::Fields variant is not supported: {:#?}", s.fields),
},
_ => panic!("The syn::Data variant is not supported: {:#?}", data),
};

eprintln! ("{:#?}", fields);

TokenStream::new()
}

Bir kez daha, terminalde cargo expand kullanarak bu kodun çıktısını görün. Alanların doğru bir şekilde çıkarılıp çıkarılmadığını onayladıktan sonra, eprintln! ifadesini kaldırabilirsiniz.

5. Güncelleme talimatlarını oluşturma

Sonraki adımda, yapının alanları üzerinde iterasyon yapıp, her alan için bir güncelleme talimatı oluşturabiliriz. Talimat, alanın adı ve tipi ile güncelleme talimatı için yeni bir işlev adı içerecek şekilde quote! makrosu kullanılarak oluşturulur.

use proc_macro::TokenStream;
use quote::*;
use syn::*;

#[proc_macro_derive(InstructionBuilder)]
pub fn instruction_builder(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input);

let fields = match data {
syn::Data::Struct(s) => match s.fields {
syn::Fields::Named(n) => n.named,
_ => panic!("The syn::Fields variant is not supported: {:#?}", s.fields),
},
_ => panic!("The syn::Data variant is not supported: {:#?}", data),
};

let update_instruction = fields.into_iter().map(|f| {
let name = &f.ident;
let ty = &f.ty;
let fname = format_ident!("update_{}", name.clone().unwrap());

quote! {
pub fn #fname(ctx: Context<UpdateAdminAccount>, new_value: #ty) -> Result<()> {
let admin_account = &mut ctx.accounts.admin_account;
admin_account.#name = new_value;
Ok(())
}
}
});

TokenStream::new()
}

6. Yeni TokenStream döndürme

Son olarak, quote! makrosunu kullanarak, ident değişkeni ile belirtilen adla yapıya ait bir uygulanabilirlik oluşturacağız. Uygulama, yapının her alanı için üretilen güncelleme talimatlarını içerir. Üretilen kod, into() yöntemi kullanılarak bir TokenStream'e dönüştürülüp, makronun sonucu olarak döndürülür.

use proc_macro::TokenStream;
use quote::*;
use syn::*;

#[proc_macro_derive(InstructionBuilder)]
pub fn instruction_builder(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input);

let fields = match data {
syn::Data::Struct(s) => match s.fields {
syn::Fields::Named(n) => n.named,
_ => panic!("The syn::Fields variant is not supported: {:#?}", s.fields),
},
_ => panic!("The syn::Data variant is not supported: {:#?}", data),
};

let update_instruction = fields.into_iter().map(|f| {
let name = &f.ident;
let ty = &f.ty;
let fname = format_ident!("update_{}", name.clone().unwrap());

quote! {
pub fn #fname(ctx: Context<UpdateAdminAccount>, new_value: #ty) -> Result<()> {
let admin_account = &mut ctx.accounts.admin_account;
admin_account.#name = new_value;
Ok(())
}
}
});

let expanded = quote! {
impl #ident {
#(#update_instruction)*
}
};
expanded.into()
}
tehlike

Makronun doğru kodu ürettiğini doğrulamak için cargo expand komutunu kullanarak makronun genişletilmiş halini görebilirsiniz. Aşağıdaki gibi bir çıktısı olacaktır:

use anchor_lang::prelude::*;
use custom_macro::InstructionBuilder;
pub struct Config {
pub auth: Pubkey,
pub bool: bool,
pub first_number: u8,
pub second_number: u64,
}
impl Config {
pub fn update_auth(
ctx: Context<UpdateAdminAccount>,
new_value: Pubkey,
) -> Result<()> {
let admin_account = &mut ctx.accounts.admin_account;
admin_account.auth = new_value;
Ok(())
}
pub fn update_bool(ctx: Context<UpdateAdminAccount>, new_value: bool) -> Result<()> {
let admin_account = &mut ctx.accounts.admin_account;
admin_account.bool = new_value;
Ok(())
}
pub fn update_first_number(
ctx: Context<UpdateAdminAccount>,
new_value: u8,
) -> Result<()> {
let admin_account = &mut ctx.accounts.admin_account;
admin_account.first_number = new_value;
Ok(())
}
pub fn update_second_number(
ctx: Context<UpdateAdminAccount>,
new_value: u64,
) -> Result<()> {
let admin_account = &mut ctx.accounts.admin_account;
admin_account.second_number = new_value;
Ok(())
}
}

7. Programı yeni makronuzu kullanacak şekilde güncelleyin

Yeni makroyu kullanarak Config yapısı için güncelleme talimatlarını üretmek için, önce programın Cargo.toml dosyasına custom-macro crate'ini bağımlılık olarak ekleyin:

[dependencies]
anchor-lang.workspace = true
custom-macro = { path = "../../custom-macro" }

Ardından state.rs dosyasına gidin ve şu kodla güncelleyin:

use crate::admin_update::UpdateAdminAccount;
use anchor_lang::prelude::*;
use custom_macro::InstructionBuilder;

#[derive(InstructionBuilder)]
#[account]
pub struct Config {
pub auth: Pubkey,
pub bool: bool,
pub first_number: u8,
pub second_number: u64,
}

impl Config {
pub const LEN: usize = Config::DISCRIMINATOR.len() + Config::INIT_SPACE;
}

Sonrasında admin_update.rs dosyasına gidin ve mevcut güncelleme talimatlarını silin; dosyada yalnızca UpdateAdminAccount bağlam yapısını bırakın.

use crate::state::Config;
use anchor_lang::prelude::*;

#[derive(Accounts)]
pub struct UpdateAdminAccount<'info> {
pub auth: Signer<'info>,
#[account(
mut,
has_one = auth,
)]
pub admin_account: Account<'info, Config>,
}

Ardından, programda lib.rs dosyasını InstructionBuilder makrosu tarafından üretilen güncelleme talimatlarını kullanacak şekilde güncelleyin.

use anchor_lang::prelude::*;
mod admin_config;
use admin_config::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod admin {
use super::*;

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Initialize::initialize(ctx)
}

pub fn update_auth(ctx: Context<UpdateAdminAccount>, new_value: Pubkey) -> Result<()> {
Config::update_auth(ctx, new_value)
}

pub fn update_bool(ctx: Context<UpdateAdminAccount>, new_value: bool) -> Result<()> {
Config::update_bool(ctx, new_value)
}

pub fn update_first_number(ctx: Context<UpdateAdminAccount>, new_value: u8) -> Result<()> {
Config::update_first_number(ctx, new_value)
}

pub fn update_second_number(ctx: Context<UpdateAdminAccount>, new_value: u64) -> Result<()> {
Config::update_second_number(ctx, new_value)
}
}

Son olarak, admin dizinine gidin ve anchor test çalıştırarak InstructionBuilder makrosu tarafından üretilen güncelleme talimatlarının doğru bir şekilde çalıştığını doğrulayın.

 admin
✔ İnitilize edildi! (160ms)
✔ Bool güncellendi! (409ms)
✔ u8 güncellendi! (403ms)
✔ u64 güncellendi! (406ms)
✔ Admin güncellendi! (405ms)

5 geçerli (2s)
ipucu

Güzel iş çıkardınız! Artık, geliştirme sürecinize yardımcı olacak işlemsel makrolar oluşturabilirsiniz. Rust dilinin imkanlarından en iyi şekilde yararlanmanızı teşvik ediyoruz; mantıklı olduğunda makroları kullanın. Ama bunların nasıl çalıştığını bilmiyorsanız bile, Anchor'ın altında neler olup bittiğini anlamanıza yardımcı olur.

Çözüm kodu ile daha fazla zamana ihtiyacınız varsa, anchor-custom-macro deposunun solution dalını referans alın.

Meydan Okuma

Öğrendiklerinizi pekiştirmek için: Başka bir işlemsel makro oluşturun. Yazdığınız ve bir makro tarafından azaltılabilir veya geliştirilebilecek kodları düşünün ve deneyin! Bu hâlâ bir pratik olduğu için, istediğiniz gibi çıkmazsa sorun değil. Hemen başlayın ve deneyin!

Kodunuzu GitHub'a yükleyin ve bize bu dersi nasıl bulduğunuzu söyleyin!